Offchain signing (#5182)

* New approach to offchain signing.

* Use in im-online

* Rewrite to use Account<T>

* DRY signing.

* Implement send_raw_unsigned_transaction

* WiP

* Expunge LocalCall

* Expunge LocalCall

* Fix compilation.

* Solve call.

* Make it compile again.

* Finalize implementation.

* Change CreateTransaction

* Clear CreateTransaction.

* Add price payload

* Send raw transaction

* Submit signed payload / unsigned transaction (WIP)

* Supertrait requirements on T::Signature

* Validate signature of payload on an unsigned transaction

* Fix encoding - part 1

* Make it compile.

* Fix compilation of unsigned validator.

* Pass price payload to the transaction

* Make block number part of the signed payload

* Send signed transaction

* Implement all_accounts, any_account

* Fix formatting

* Implement submit_transaction

* Submit signed transaction (ForAll, ForAny)

* Fix formatting

* Implement CreateSignedTransaction

* Move sign and verify to AppCrypto

* Sign transaction

* Call `use_encoded`

* Remove SubmitAndSignTransaction

* Implement runtime using new SigningTypes

* Adapt offchain example to changes

* Fix im-online pallet

* Quick fix: rename AuthorityId2

* Fix offchain example tests

* Add a comment on why keystore is required in unsigned transaction test

* Use UintAuthorityId instead of u64

* WIP

* Remove IdentifyAccount from UintAuthorityId

* Implement PublicWrapper type

* Fix im-online tests

* Fix runtime test

* Bump spec version

* Fix executor tests

* Rename ImOnlineAuthId -> ImOnlineAuthorityId and formatting

* Fix merge

* Documentation

* Revert u64 -> UintAuthorityId conversion

* Fix string errors

* Document public members in offchain module

* Introduce SubmitTransaction

* Update pallets to use SubmitTransaction

* WIP

* Use SubmitTransaction in offchain

* Use `submit_unsigned_transaction`

* Fix tests

* Update docs

* Remove SigningTypes requirement from `SendTransactionTypes`

* Fix tests

* Update frame/system/src/offchain.rs

Co-Authored-By: Kian Paimani <5588131+kianenigma@users.noreply.github.com>

* Update frame/system/src/offchain.rs

Co-Authored-By: Kian Paimani <5588131+kianenigma@users.noreply.github.com>

* Update frame/example-offchain-worker/src/tests.rs

Co-Authored-By: Kian Paimani <5588131+kianenigma@users.noreply.github.com>

* Update frame/system/src/offchain.rs

Co-Authored-By: Kian Paimani <5588131+kianenigma@users.noreply.github.com>

* Update frame/system/src/offchain.rs

Co-Authored-By: Kian Paimani <5588131+kianenigma@users.noreply.github.com>

* Remove leftover from previous iterations

* Change enum to struct

* Remove public

* Move mock to node/executor/tests

* Cleanup test-helpers

* Make `application-crypto` `std` feature internal

The macros should not generate code that requires that the calling crate
has a feature with the name `std` defined.

* Revert cargo lock update

* Use TestAuthorityId from common

* Restore members of account to public

* Tidy up imports

* Fix benchmarking pallet

* Add tests demonstrating ForAll, ForAny on signer

* Move definition of AppCrypto

in example-offchain-worker
from tests to mod::crypto

* Cleanup stray comment

* Fix ValidTransaction

* Re-fix CreateSignedTransaction

* Address PR feedback

* Add can_sign method to signer

* Propagate error

* Improve documentation

* Fix vec! macro not available

* Document SendTransactiontypes

* Add some docs.

* Split signing examples

* Add tests for signing examples

* WIP can_sign - PR feedback

* WIP

* Split for_any / for_all into different calls

* Verify payload and signature in test

* Fix can_sign implementation

* Fix impl_version

* Import Box from sp_std

* Create issues for TODOs

* Ignore doctest.

* Add test directly to system. Adjust UintTypes.

* Add some tests to account filtering.

* Remove code samples and point to example offchain worker

* Fix doc links

* Fix im-online tests using signatures.

Co-authored-by: Tomasz Drwięga <tomasz@parity.io>
Co-authored-by: Kian Paimani <5588131+kianenigma@users.noreply.github.com>
Co-authored-by: Bastian Köcher <git@kchr.de>
This commit is contained in:
Rakan Alhneiti
2020-04-21 14:55:05 +02:00
committed by GitHub
parent 798de8337b
commit 25751c0562
18 changed files with 1482 additions and 619 deletions
+32 -2
View File
@@ -15,10 +15,21 @@
// along with Substrate. If not, see <http://www.gnu.org/licenses/>.
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<MultiSigner, MultiSignature> 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
@@ -15,24 +15,29 @@
// along with Substrate. If not, see <http://www.gnu.org/licenses/>.
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<pallet_im_online::sr25519::AuthorityId>;
#[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);
<SubmitTransaction as SubmitUnsignedTransaction<Runtime, Call>>
::submit_unsigned(call)
SubmitTransaction::<Runtime, pallet_im_online::Call<Runtime>>::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 = <SubmitTransaction as SubmitSignedTransaction<Runtime, Call>>
::find_all_local_keys();
assert_eq!(keys.len(), 3, "Missing keys: {:?}", keys);
let can_sign = <SubmitTransaction as SubmitSignedTransaction<Runtime, Call>>
::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 =
<SubmitTransaction as SubmitSignedTransaction<Runtime, Call>>::submit_signed(call);
let results = Signer::<Runtime, TestAuthorityId>::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 =
<SubmitTransaction as SubmitSignedTransaction<Runtime, Call>>::submit_signed(call);
let result = Signer::<Runtime, TestAuthorityId>::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 =
<SubmitTransaction as SubmitSignedTransaction<Runtime, Call>>::submit_signed(call);
let result = Signer::<Runtime, TestAuthorityId>::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::<Runtime, TestAuthorityId>::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::<Runtime, TestAuthorityId>::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<Runtime> {
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 =
<SubmitTransaction as SubmitSignedTransaction<Runtime, Call>>::submit_signed(call);
let results = Signer::<Runtime, TestAuthorityId>::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);
+60 -67
View File
@@ -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<KeyType> = TransactionSubmitter<KeyType, Runtime, UncheckedExtrinsic>;
/// Submits transaction with the node's public and signature type. Adheres to the signed extension
/// format of the chain.
impl frame_system::offchain::CreateTransaction<Runtime, UncheckedExtrinsic> for Runtime {
type Public = <Signature as traits::Verify>::Signer;
type Signature = Signature;
fn create_transaction<TSigner: frame_system::offchain::Signer<Self::Public, Self::Signature>>(
call: Call,
public: Self::Public,
account: AccountId,
index: Index,
) -> Option<(Call, <UncheckedExtrinsic as traits::Extrinsic>::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::<u64>()
// 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::<Runtime>::new(),
frame_system::CheckGenesis::<Runtime>::new(),
frame_system::CheckEra::<Runtime>::from(generic::Era::mortal(period, current_block)),
frame_system::CheckNonce::<Runtime>::from(index),
frame_system::CheckWeight::<Runtime>::new(),
pallet_transaction_payment::ChargeTransactionPayment::<Runtime>::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<LocalCall> frame_system::offchain::CreateSignedTransaction<LocalCall> for Runtime where
Call: From<LocalCall>,
{
fn create_transaction<C: frame_system::offchain::AppCrypto<Self::Public, Self::Signature>>(
call: Call,
public: <Signature as traits::Verify>::Signer,
account: AccountId,
nonce: Index,
) -> Option<(Call, <UncheckedExtrinsic as traits::Extrinsic>::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::<u64>()
// 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::<Runtime>::new(),
frame_system::CheckGenesis::<Runtime>::new(),
frame_system::CheckEra::<Runtime>::from(generic::Era::mortal(period, current_block)),
frame_system::CheckNonce::<Runtime>::from(nonce),
frame_system::CheckWeight::<Runtime>::new(),
pallet_transaction_payment::ChargeTransactionPayment::<Runtime>::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 = <Signature as traits::Verify>::Signer;
type Signature = Signature;
}
impl<C> frame_system::offchain::SendTransactionTypes<C> for Runtime where
Call: From<C>,
{
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<Self::AuthorityId>;
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<T>() where
T: SubmitSignedTransaction<
Runtime,
Call,
>,
T: CreateSignedTransaction<Call>,
{}
fn is_sign_and_submit_transaction<T>() where
T: SignAndSubmitTransaction<
Runtime,
Call,
Extrinsic=UncheckedExtrinsic,
CreateTransaction=Runtime,
Signer=ImOnlineId,
>,
{}
is_submit_signed_transaction::<TransactionSubmitterOf<ImOnlineId>>();
is_sign_and_submit_transaction::<TransactionSubmitterOf<ImOnlineId>>();
is_submit_signed_transaction::<Runtime>();
}
}
@@ -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<<Sr25519Signature as Verify>::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<Self, <Self as Trait>::Call>;
/// The type to submit unsigned transactions.
type SubmitUnsignedTransaction:
offchain::SubmitUnsignedTransaction<Self, <Self as Trait>::Call>;
pub trait Trait: CreateSignedTransaction<Call<Self>> {
/// The identifier type for an offchain worker.
type AuthorityId: AppCrypto<Self::Public, Self::Signature>;
/// The overarching event type.
type Event: From<Event<Self>> + Into<<Self as frame_system::Trait>::Event>;
@@ -115,6 +132,21 @@ pub trait Trait: frame_system::Trait {
type UnsignedPriority: Get<TransactionPriority>;
}
/// 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<Public, BlockNumber> {
block_number: BlockNumber,
price: u32,
public: Public,
}
impl<T: SigningTypes> SignedPayload<T> for PricePayload<T::Public, T::BlockNumber> {
fn public(&self) -> T::Public {
self.public.clone()
}
}
decl_storage! {
trait Store for Module<T: Trait> 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<T::Public, T::BlockNumber>,
_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 = <system::Module<T>>::block_number();
<NextUnsignedAt<T>>::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<T: Trait> Module<T> {
// 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<T: Trait> Module<T> {
/// 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::<T, T::AuthorityId>::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 = <NextUnsignedAt<T>>::get();
@@ -385,14 +435,101 @@ impl<T: Trait> Module<T> {
// 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::<T, Call<T>>::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 = <NextUnsignedAt<T>>::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::<T, Call<T>>::submit_unsigned_transaction(call.into())
.map_err(|()| "Unable to submit unsigned transaction.")?;
// -- Sign using any account
let (_, result) = Signer::<T, T::AuthorityId>::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 = <NextUnsignedAt<T>>::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::<T, Call<T>>::submit_unsigned_transaction(call.into())
.map_err(|()| "Unable to submit unsigned transaction.")?;
// -- Sign using all accounts
let transaction_results = Signer::<T, T::AuthorityId>::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<T: Trait> Module<T> {
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 = <NextUnsignedAt<T>>::get();
if &next_unsigned_at > block_number {
return InvalidTransaction::Stale.into();
}
// Let's make sure to reject transactions from the future.
let current_block = <system::Module<T>>::block_number();
if &current_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<T: Trait> frame_support::unsigned::ValidateUnsigned for Module<T> {
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 = <NextUnsignedAt<T>>::get();
if &next_unsigned_at > block_number {
return InvalidTransaction::Stale.into();
if let Call::submit_price_unsigned_with_signed_payload(
ref payload, ref signature
) = call {
let signature_valid = SignedPayload::<T>::verify::<T::AuthorityId>(payload, signature.clone());
if !signature_valid {
return InvalidTransaction::BadProof.into();
}
// Let's make sure to reject transactions from the future.
let current_block = <system::Module<T>>::block_number();
if &current_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 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()
}
@@ -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<Call<Test>, ()>;
type SubmitTransaction = frame_system::offchain::TransactionSubmitter<
crypto::Public,
Test,
Extrinsic
>;
type AccountId = <<Signature as Verify>::Signer as IdentifyAccount>::AccountId;
impl frame_system::offchain::CreateTransaction<Test, Extrinsic> for Test {
type Public = sp_core::sr25519::Public;
type Signature = sp_core::sr25519::Signature;
impl frame_system::offchain::SigningTypes for Test {
type Public = <Signature as Verify>::Signer;
type Signature = Signature;
}
fn create_transaction<F: frame_system::offchain::Signer<Self::Public, Self::Signature>>(
call: <Extrinsic as ExtrinsicsT>::Call,
_public: Self::Public,
_account: <Test as frame_system::Trait>::AccountId,
nonce: <Test as frame_system::Trait>::Index,
) -> Option<(<Extrinsic as ExtrinsicsT>::Call, <Extrinsic as ExtrinsicsT>::SignaturePayload)> {
impl<LocalCall> frame_system::offchain::SendTransactionTypes<LocalCall> for Test where
Call<Test>: From<LocalCall>,
{
type OverarchingCall = Call<Test>;
type Extrinsic = Extrinsic;
}
impl<LocalCall> frame_system::offchain::CreateSignedTransaction<LocalCall> for Test where
Call<Test>: From<LocalCall>,
{
fn create_transaction<C: frame_system::offchain::AppCrypto<Self::Public, Self::Signature>>(
call: Call<Test>,
_public: <Signature as Verify>::Signer,
_account: AccountId,
nonce: u64,
) -> Option<(Call<Test>, <Extrinsic as ExtrinsicT>::SignaturePayload)> {
Some((call, (nonce, ())))
}
}
@@ -100,9 +111,8 @@ parameter_types! {
impl Trait for Test {
type Event = ();
type AuthorityId = crypto::TestAuthId;
type Call = Call<Test>;
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: <Test as SigningTypes>::Public::from(public_key),
};
// let signature = price_payload.sign::<crypto::TestAuthId>().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 = <PricePayload<
<Test as SigningTypes>::Public,
<Test as frame_system::Trait>::BlockNumber
> as SignedPayload<Test>>::verify::<crypto::TestAuthId>(&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: <Test as SigningTypes>::Public::from(public_key),
};
// let signature = price_payload.sign::<crypto::TestAuthId>().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 = <PricePayload<
<Test as SigningTypes>::Public,
<Test as frame_system::Trait>::BlockNumber
> as SignedPayload<Test>>::verify::<crypto::TestAuthId>(&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());
+24 -10
View File
@@ -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<BlockNumber>
pub authority_index: AuthIndex,
}
pub trait Trait: frame_system::Trait + pallet_session::historical::Trait {
pub trait Trait: SendTransactionTypes<Call<Self>> + pallet_session::historical::Trait {
/// The identifier type for an authority.
type AuthorityId: Member + Parameter + RuntimeAppPublic + Default + Ord;
/// The overarching event type.
type Event: From<Event<Self>> + Into<<Self as frame_system::Trait>::Event>;
/// A dispatchable call type.
type Call: From<Call<Self>>;
/// A transaction submitter.
type SubmitTransaction: SubmitUnsignedTransaction<Self, <Self as Trait>::Call>;
/// An expected duration of the session.
///
/// This parameter is used to determine the longevity of `heartbeat` transaction
@@ -444,6 +441,7 @@ impl<T: Trait> Module<T> {
}
let session_index = <pallet_session::Module<T>>::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<T: Trait> Module<T> {
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<T: Trait> Module<T> {
call,
);
T::SubmitTransaction::submit_unsigned(call)
SubmitTransaction::<T, Call<T>>::submit_unsigned_transaction(call.into())
.map_err(|_| OffchainErr::SubmitTransaction)?;
Ok(())
@@ -501,9 +501,18 @@ impl<T: Trait> Module<T> {
}
fn local_authority_keys() -> impl Iterator<Item=(u32, T::AuthorityId)> {
// 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::<T>::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<T: Trait> Module<T> {
Keys::<T>::put(keys);
}
}
#[cfg(test)]
fn set_keys(keys: Vec<T::AuthorityId>) {
Keys::<T>::put(&keys)
}
}
impl<T: Trait> sp_runtime::BoundToRuntimeAppPublic for Module<T> {
+14 -6
View File
@@ -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<Option<Vec<u64>>> = RefCell::new(Some(vec![1, 2, 3]));
pub static VALIDATORS: RefCell<Option<Vec<u64>>> = RefCell::new(Some(vec![
1,
2,
3,
]));
}
pub struct TestSessionManager;
@@ -68,7 +71,6 @@ impl pallet_session::historical::SessionManager<u64, u64> for TestSessionManager
/// An extrinsic type used for tests.
pub type Extrinsic = TestXt<Call, ()>;
type SubmitTransaction = frame_system::offchain::TransactionSubmitter<(), Call, Extrinsic>;
type IdentificationTuple = (u64, u64);
type Offence = crate::UnresponsivenessOffence<IdentificationTuple>;
@@ -90,7 +92,6 @@ pub fn new_test_ext() -> sp_io::TestExternalities {
t.into()
}
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct Runtime;
@@ -168,13 +169,18 @@ parameter_types! {
impl Trait for Runtime {
type AuthorityId = UintAuthorityId;
type Event = ();
type Call = Call;
type SubmitTransaction = SubmitTransaction;
type ReportUnresponsiveness = OffenceHandler;
type SessionDuration = Period;
type UnsignedPriority = UnsignedPriority;
}
impl<LocalCall> frame_system::offchain::SendTransactionTypes<LocalCall> for Runtime where
Call: From<LocalCall>,
{
type OverarchingCall = Call;
type Extrinsic = Extrinsic;
}
/// Im Online module.
pub type ImOnline = Module<Runtime>;
pub type System = frame_system::Module<Runtime>;
@@ -184,5 +190,7 @@ pub fn advance_session() {
let now = System::block_number().max(1);
System::set_block_number(now + 1);
Session::rotate_session();
let keys = Session::validators().into_iter().map(UintAuthorityId).collect();
ImOnline::set_keys(keys);
assert_eq!(Session::current_index(), (now / Period::get()) as u32);
}
+6 -6
View File
@@ -61,15 +61,15 @@ fn should_report_offline_validators() {
let block = 1;
System::set_block_number(block);
// buffer new validators
Session::rotate_session();
advance_session();
// enact the change and buffer another one
let validators = vec![1, 2, 3, 4, 5, 6];
VALIDATORS.with(|l| *l.borrow_mut() = Some(validators.clone()));
Session::rotate_session();
advance_session();
// when
// we end current session and start the next one
Session::rotate_session();
advance_session();
// then
let offences = OFFENCES.with(|l| l.replace(vec![]));
@@ -89,7 +89,7 @@ fn should_report_offline_validators() {
for (idx, v) in validators.into_iter().take(4).enumerate() {
let _ = heartbeat(block, 3, idx as u32, v.into()).unwrap();
}
Session::rotate_session();
advance_session();
// then
let offences = OFFENCES.with(|l| l.replace(vec![]));
@@ -174,7 +174,7 @@ fn late_heartbeat_should_fail() {
new_test_ext().execute_with(|| {
advance_session();
// given
VALIDATORS.with(|l| *l.borrow_mut() = Some(vec![1, 2, 4, 4, 5, 6]));
VALIDATORS.with(|l| *l.borrow_mut() = Some(vec![1, 2, 3, 4, 5, 6]));
assert_eq!(Session::validators(), Vec::<u64>::new());
// enact the change and buffer another one
advance_session();
@@ -315,7 +315,7 @@ fn should_not_send_a_report_if_already_online() {
ImOnline::note_uncle(3, 0);
// when
UintAuthorityId::set_all_keys(vec![0]); // all authorities use pallet_session key 0
UintAuthorityId::set_all_keys(vec![1, 2, 3]);
// we expect error, since the authority is already online.
let mut res = ImOnline::send_heartbeats(4).unwrap();
assert_eq!(res.next().unwrap().unwrap(), ());
@@ -151,11 +151,13 @@ parameter_types! {
}
pub type Extrinsic = sp_runtime::testing::TestXt<Call, ()>;
type SubmitTransaction = frame_system::offchain::TransactionSubmitter<
sp_runtime::testing::UintAuthorityId,
Test,
Extrinsic,
>;
impl<C> frame_system::offchain::SendTransactionTypes<C> for Test where
Call: From<C>,
{
type OverarchingCall = Call;
type Extrinsic = Extrinsic;
}
impl pallet_staking::Trait for Test {
type Currency = Balances;
@@ -174,7 +176,6 @@ impl pallet_staking::Trait for Test {
type NextNewSession = Session;
type ElectionLookahead = ();
type Call = Call;
type SubmitTransaction = SubmitTransaction;
type MaxNominatorRewardedPerValidator = MaxNominatorRewardedPerValidator;
type UnsignedPriority = UnsignedPriority;
}
+3 -6
View File
@@ -320,7 +320,7 @@ use sp_staking::{
use sp_runtime::{Serialize, Deserialize};
use frame_system::{
self as system, ensure_signed, ensure_root, ensure_none,
offchain::SubmitUnsignedTransaction,
offchain::SendTransactionTypes,
};
use sp_phragmen::{
ExtendedBalance, Assignment, PhragmenScore, PhragmenResult, build_support_map, evaluate_support,
@@ -743,7 +743,7 @@ impl<T: Trait> SessionInterface<<T as frame_system::Trait>::AccountId> for T whe
}
}
pub trait Trait: frame_system::Trait {
pub trait Trait: frame_system::Trait + SendTransactionTypes<Call<Self>> {
/// The staking balance.
type Currency: LockableCurrency<Self::AccountId, Moment=Self::BlockNumber>;
@@ -804,10 +804,7 @@ pub trait Trait: frame_system::Trait {
/// The overarching call type.
type Call: Dispatchable + From<Call<Self>> + IsSubType<Module<Self>, Self> + Clone;
/// A transaction submitter.
type SubmitTransaction: SubmitUnsignedTransaction<Self, <Self as Trait>::Call>;
/// The maximum number of nominators rewarded for each validator.
/// The maximum number of nominator rewarded for each validator.
///
/// For each validator only the `$MaxNominatorRewardedPerValidator` biggest stakers can claim
/// their reward. This used to limit the i/o cost for the nominator payout.
+7 -3
View File
@@ -29,7 +29,6 @@ use frame_support::{
traits::{Currency, Get, FindAuthor, OnFinalize, OnInitialize},
weights::Weight,
};
use frame_system::offchain::TransactionSubmitter;
use sp_io;
use sp_phragmen::{
build_support_map, evaluate_support, reduce, ExtendedBalance, StakedAssignment, PhragmenScore,
@@ -309,13 +308,18 @@ impl Trait for Test {
type NextNewSession = Session;
type ElectionLookahead = ElectionLookahead;
type Call = Call;
type SubmitTransaction = SubmitTransaction;
type MaxNominatorRewardedPerValidator = MaxNominatorRewardedPerValidator;
type UnsignedPriority = UnsignedPriority;
}
impl<LocalCall> frame_system::offchain::SendTransactionTypes<LocalCall> for Test where
Call: From<LocalCall>,
{
type OverarchingCall = Call;
type Extrinsic = Extrinsic;
}
pub type Extrinsic = TestXt<Call, ()>;
type SubmitTransaction = TransactionSubmitter<(), Test, Extrinsic>;
pub struct ExtBuilder {
session_length: BlockNumber,
@@ -19,7 +19,7 @@
use crate::{
Call, CompactAssignments, Module, NominatorIndex, OffchainAccuracy, Trait, ValidatorIndex,
};
use frame_system::offchain::SubmitUnsignedTransaction;
use frame_system::offchain::SubmitTransaction;
use sp_phragmen::{
build_support_map, evaluate_support, reduce, Assignment, ExtendedBalance, PhragmenResult,
PhragmenScore,
@@ -117,14 +117,14 @@ pub(crate) fn compute_offchain_election<T: Trait>() -> Result<(), OffchainElecti
let current_era = <Module<T>>::current_era().unwrap_or_default();
// send it.
let call: <T as Trait>::Call = Call::submit_election_solution_unsigned(
let call = Call::submit_election_solution_unsigned(
winners,
compact,
score,
current_era,
).into();
T::SubmitTransaction::submit_unsigned(call)
SubmitTransaction::<T, Call<T>>::submit_unsigned_transaction(call)
.map_err(|_| OffchainElectionError::PoolSubmissionFailed)
}
+6 -5
View File
@@ -1591,7 +1591,7 @@ impl<T: Trait> Lookup for ChainContext<T> {
}
#[cfg(test)]
mod tests {
pub(crate) mod tests {
use super::*;
use sp_std::cell::RefCell;
use sp_core::H256;
@@ -1602,7 +1602,7 @@ mod tests {
pub enum Origin for Test where system = super {}
}
#[derive(Clone, Eq, PartialEq)]
#[derive(Clone, Eq, PartialEq, Debug)]
pub struct Test;
parameter_types! {
@@ -1630,8 +1630,9 @@ mod tests {
fn on_killed_account(who: &u64) { KILLED.with(|r| r.borrow_mut().push(*who)) }
}
#[derive(Debug)]
pub struct Call {}
#[derive(Debug, codec::Encode, codec::Decode)]
pub struct Call;
impl Dispatchable for Call {
type Origin = ();
type Trait = ();
@@ -1679,7 +1680,7 @@ mod tests {
type System = Module<Test>;
const CALL: &<Test as Trait>::Call = &Call {};
const CALL: &<Test as Trait>::Call = &Call;
fn new_test_ext() -> sp_io::TestExternalities {
GenesisConfig::default().build_storage::<Test>().unwrap().into()
File diff suppressed because it is too large Load Diff
@@ -25,7 +25,7 @@ pub use sp_core::{self, crypto::{CryptoType, CryptoTypePublicPair, Public, Deriv
#[doc(hidden)]
#[cfg(feature = "full_crypto")]
pub use sp_core::crypto::{SecretStringError, DeriveJunction, Ss58Codec, Pair};
pub use sp_core::crypto::{CryptoTypeId, KeyTypeId, key_types};
pub use sp_core::crypto::{KeyTypeId, key_types};
#[doc(hidden)]
pub use codec;
@@ -320,29 +320,10 @@ mod tests {
use super::*;
use sp_io::hashing::blake2_256;
use crate::codec::{Encode, Decode};
use crate::traits::{SignedExtension, IdentifyAccount, IdentityLookup};
use serde::{Serialize, Deserialize};
use crate::traits::{SignedExtension, IdentityLookup};
use crate::testing::TestSignature as TestSig;
type TestContext = IdentityLookup<u64>;
#[derive(Eq, PartialEq, Clone, Copy, Debug, Serialize, Deserialize, Encode, Decode)]
pub struct TestSigner(pub u64);
impl From<u64> for TestSigner { fn from(x: u64) -> Self { Self(x) } }
impl From<TestSigner> for u64 { fn from(x: TestSigner) -> Self { x.0 } }
impl IdentifyAccount for TestSigner {
type AccountId = u64;
fn into_account(self) -> u64 { self.into() }
}
#[derive(Eq, PartialEq, Clone, Debug, Serialize, Deserialize, Encode, Decode)]
struct TestSig(u64, Vec<u8>);
impl traits::Verify for TestSig {
type Signer = TestSigner;
fn verify<L: traits::Lazy<[u8]>>(&self, mut msg: L, signer: &u64) -> bool {
signer == &self.0 && msg.get() == &self.1[..]
}
}
type TestAccountId = u64;
type TestCall = Vec<u8>;
+21
View File
@@ -182,18 +182,39 @@ impl From<ed25519::Signature> for MultiSignature {
}
}
impl TryFrom<MultiSignature> for ed25519::Signature {
type Error = ();
fn try_from(m: MultiSignature) -> Result<Self, Self::Error> {
if let MultiSignature::Ed25519(x) = m { Ok(x) } else { Err(()) }
}
}
impl From<sr25519::Signature> for MultiSignature {
fn from(x: sr25519::Signature) -> Self {
MultiSignature::Sr25519(x)
}
}
impl TryFrom<MultiSignature> for sr25519::Signature {
type Error = ();
fn try_from(m: MultiSignature) -> Result<Self, Self::Error> {
if let MultiSignature::Sr25519(x) = m { Ok(x) } else { Err(()) }
}
}
impl From<ecdsa::Signature> for MultiSignature {
fn from(x: ecdsa::Signature) -> Self {
MultiSignature::Ecdsa(x)
}
}
impl TryFrom<MultiSignature> for ecdsa::Signature {
type Error = ();
fn try_from(m: MultiSignature) -> Result<Self, Self::Error> {
if let MultiSignature::Ecdsa(x) = m { Ok(x) } else { Err(()) }
}
}
impl Default for MultiSignature {
fn default() -> Self {
MultiSignature::Ed25519(Default::default())
+31 -19
View File
@@ -17,7 +17,7 @@
//! Testing utilities.
use serde::{Serialize, Serializer, Deserialize, de::Error as DeError, Deserializer};
use std::{fmt::Debug, ops::Deref, fmt, cell::RefCell};
use std::{fmt::{self, Debug}, ops::Deref, cell::RefCell};
use crate::codec::{Codec, Encode, Decode};
use crate::traits::{
self, Checkable, Applyable, BlakeTwo256, OpaqueKeys,
@@ -29,7 +29,12 @@ pub use sp_core::{H256, sr25519};
use sp_core::{crypto::{CryptoType, Dummy, key_types, Public}, U256};
use crate::transaction_validity::{TransactionValidity, TransactionValidityError, TransactionSource};
/// Authority Id
/// A dummy type which can be used instead of regular cryptographic primitives.
///
/// 1. Wraps a `u64` `AccountId` and is able to `IdentifyAccount`.
/// 2. Can be converted to any `Public` key.
/// 3. Implements `RuntimeAppPublic` so it can be used instead of regular application-specific
/// crypto.
#[derive(Default, PartialEq, Eq, Clone, Encode, Decode, Debug, Hash, Serialize, Deserialize, PartialOrd, Ord)]
pub struct UintAuthorityId(pub u64);
@@ -82,7 +87,7 @@ impl UintAuthorityId {
impl sp_application_crypto::RuntimeAppPublic for UintAuthorityId {
const ID: KeyTypeId = key_types::DUMMY;
type Signature = u64;
type Signature = TestSignature;
fn all() -> Vec<Self> {
ALL_KEYS.with(|l| l.borrow().clone())
@@ -94,25 +99,11 @@ impl sp_application_crypto::RuntimeAppPublic for UintAuthorityId {
}
fn sign<M: AsRef<[u8]>>(&self, msg: &M) -> Option<Self::Signature> {
let mut signature = [0u8; 8];
msg.as_ref().iter()
.chain(std::iter::repeat(&42u8))
.take(8)
.enumerate()
.for_each(|(i, v)| { signature[i] = *v; });
Some(u64::from_le_bytes(signature))
Some(TestSignature(self.0, msg.as_ref().to_vec()))
}
fn verify<M: AsRef<[u8]>>(&self, msg: &M, signature: &Self::Signature) -> bool {
let mut msg_signature = [0u8; 8];
msg.as_ref().iter()
.chain(std::iter::repeat(&42))
.take(8)
.enumerate()
.for_each(|(i, v)| { msg_signature[i] = *v; });
u64::from_le_bytes(msg_signature) == *signature
traits::Verify::verify(signature, msg.as_ref(), &self.0)
}
fn to_raw_vec(&self) -> Vec<u8> {
@@ -140,6 +131,26 @@ impl crate::BoundToRuntimeAppPublic for UintAuthorityId {
type Public = Self;
}
impl traits::IdentifyAccount for UintAuthorityId {
type AccountId = u64;
fn into_account(self) -> Self::AccountId {
self.0
}
}
/// A dummy signature type, to match `UintAuthorityId`.
#[derive(Eq, PartialEq, Clone, Debug, Hash, Serialize, Deserialize, Encode, Decode)]
pub struct TestSignature(pub u64, pub Vec<u8>);
impl traits::Verify for TestSignature {
type Signer = UintAuthorityId;
fn verify<L: traits::Lazy<[u8]>>(&self, mut msg: L, signer: &u64) -> bool {
signer == &self.0 && msg.get() == &self.1[..]
}
}
/// Digest item
pub type DigestItem = generic::DigestItem<H256>;
@@ -332,6 +343,7 @@ impl<Call: Codec + Sync + Send, Context, Extra> Checkable<Context> for TestXt<Ca
type Checked = Self;
fn check(self, _: &Context) -> Result<Self::Checked, TransactionValidityError> { Ok(self) }
}
impl<Call: Codec + Sync + Send, Extra> traits::Extrinsic for TestXt<Call, Extra> {
type Call = Call;
type SignaturePayload = (u64, Extra);