#![warn(missing_docs)]
use codec::Encode;
use sp_std::collections::btree_set::BTreeSet;
use sp_std::convert::{TryInto, TryFrom};
use sp_std::prelude::{Box, Vec};
use sp_runtime::app_crypto::RuntimeAppPublic;
use sp_runtime::traits::{Extrinsic as ExtrinsicT, IdentifyAccount, One};
use frame_support::{debug, storage::StorageMap, RuntimeDebug};
pub struct ForAll {}
pub struct ForAny {}
pub struct SubmitTransaction<T: SendTransactionTypes<OverarchingCall>, OverarchingCall> {
_phantom: sp_std::marker::PhantomData<(T, OverarchingCall)>
}
impl<T, LocalCall> SubmitTransaction<T, LocalCall>
where
T: SendTransactionTypes<LocalCall>,
{
pub fn submit_transaction(
call: <T as SendTransactionTypes<LocalCall>>::OverarchingCall,
signature: Option<<T::Extrinsic as ExtrinsicT>::SignaturePayload>,
) -> Result<(), ()> {
let xt = T::Extrinsic::new(call.into(), signature).ok_or(())?;
sp_io::offchain::submit_transaction(xt.encode())
}
pub fn submit_unsigned_transaction(
call: <T as SendTransactionTypes<LocalCall>>::OverarchingCall,
) -> Result<(), ()> {
SubmitTransaction::<T, LocalCall>::submit_transaction(call, None)
}
}
#[derive(RuntimeDebug)]
pub struct Signer<T: SigningTypes, C: AppCrypto<T::Public, T::Signature>, X = ForAny> {
accounts: Option<Vec<T::Public>>,
_phantom: sp_std::marker::PhantomData<(X, C)>,
}
impl<T: SigningTypes, C: AppCrypto<T::Public, T::Signature>, X> Default for Signer<T, C, X> {
fn default() -> Self {
Self {
accounts: Default::default(),
_phantom: Default::default(),
}
}
}
impl<T: SigningTypes, C: AppCrypto<T::Public, T::Signature>, X> Signer<T, C, X> {
pub fn all_accounts() -> Signer<T, C, ForAll> {
Default::default()
}
pub fn any_account() -> Signer<T, C, ForAny> {
Default::default()
}
pub fn with_filter(mut self, accounts: Vec<T::Public>) -> Self {
self.accounts = Some(accounts);
self
}
pub fn can_sign(&self) -> bool {
self.accounts_from_keys().count() > 0
}
fn accounts_from_keys<'a>(&'a self) -> Box<dyn Iterator<Item = Account<T>> + 'a> {
let keystore_accounts = self.keystore_accounts();
match self.accounts {
None => Box::new(keystore_accounts),
Some(ref keys) => {
let keystore_lookup: BTreeSet<<T as SigningTypes>::Public> = keystore_accounts
.map(|account| account.public).collect();
Box::new(keys.into_iter()
.enumerate()
.map(|(index, key)| {
let account_id = key.clone().into_account();
Account::new(index, account_id, key.clone())
})
.filter(move |account| keystore_lookup.contains(&account.public)))
}
}
}
fn keystore_accounts(&self) -> impl Iterator<Item = Account<T>> {
C::RuntimeAppPublic::all()
.into_iter()
.enumerate()
.map(|(index, key)| {
let generic_public = C::GenericPublic::from(key);
let public = generic_public.into();
let account_id = public.clone().into_account();
Account::new(index, account_id, public.clone())
})
}
}
impl<T: SigningTypes, C: AppCrypto<T::Public, T::Signature>> Signer<T, C, ForAll> {
fn for_all<F, R>(&self, f: F) -> Vec<(Account<T>, R)> where
F: Fn(&Account<T>) -> Option<R>,
{
let accounts = self.accounts_from_keys();
accounts
.into_iter()
.filter_map(|account| {
f(&account).map(|res| (account, res))
})
.collect()
}
}
impl<T: SigningTypes, C: AppCrypto<T::Public, T::Signature>> Signer<T, C, ForAny> {
fn for_any<F, R>(&self, f: F) -> Option<(Account<T>, R)> where
F: Fn(&Account<T>) -> Option<R>,
{
let accounts = self.accounts_from_keys();
for account in accounts.into_iter() {
let res = f(&account);
if let Some(res) = res {
return Some((account, res));
}
}
None
}
}
impl<T: SigningTypes, C: AppCrypto<T::Public, T::Signature>> SignMessage<T> for Signer<T, C, ForAll> {
type SignatureData = Vec<(Account<T>, T::Signature)>;
fn sign_message(&self, message: &[u8]) -> Self::SignatureData {
self.for_all(|account| C::sign(message, account.public.clone()))
}
fn sign<TPayload, F>(&self, f: F) -> Self::SignatureData where
F: Fn(&Account<T>) -> TPayload,
TPayload: SignedPayload<T>,
{
self.for_all(|account| f(account).sign::<C>())
}
}
impl<T: SigningTypes, C: AppCrypto<T::Public, T::Signature>> SignMessage<T> for Signer<T, C, ForAny> {
type SignatureData = Option<(Account<T>, T::Signature)>;
fn sign_message(&self, message: &[u8]) -> Self::SignatureData {
self.for_any(|account| C::sign(message, account.public.clone()))
}
fn sign<TPayload, F>(&self, f: F) -> Self::SignatureData where
F: Fn(&Account<T>) -> TPayload,
TPayload: SignedPayload<T>,
{
self.for_any(|account| f(account).sign::<C>())
}
}
impl<
T: CreateSignedTransaction<LocalCall> + SigningTypes,
C: AppCrypto<T::Public, T::Signature>,
LocalCall,
> SendSignedTransaction<T, C, LocalCall> for Signer<T, C, ForAny> {
type Result = Option<(Account<T>, Result<(), ()>)>;
fn send_signed_transaction(
&self,
f: impl Fn(&Account<T>) -> LocalCall,
) -> Self::Result {
self.for_any(|account| {
let call = f(account);
self.send_single_signed_transaction(account, call)
})
}
}
impl<
T: SigningTypes + CreateSignedTransaction<LocalCall>,
C: AppCrypto<T::Public, T::Signature>,
LocalCall,
> SendSignedTransaction<T, C, LocalCall> for Signer<T, C, ForAll> {
type Result = Vec<(Account<T>, Result<(), ()>)>;
fn send_signed_transaction(
&self,
f: impl Fn(&Account<T>) -> LocalCall,
) -> Self::Result {
self.for_all(|account| {
let call = f(account);
self.send_single_signed_transaction(account, call)
})
}
}
impl<
T: SigningTypes + SendTransactionTypes<LocalCall>,
C: AppCrypto<T::Public, T::Signature>,
LocalCall,
> SendUnsignedTransaction<T, LocalCall> for Signer<T, C, ForAny> {
type Result = Option<(Account<T>, Result<(), ()>)>;
fn send_unsigned_transaction<TPayload, F>(
&self,
f: F,
f2: impl Fn(TPayload, T::Signature) -> LocalCall,
) -> Self::Result
where
F: Fn(&Account<T>) -> TPayload,
TPayload: SignedPayload<T>,
{
self.for_any(|account| {
let payload = f(account);
let signature= payload.sign::<C>()?;
let call = f2(payload, signature);
self.submit_unsigned_transaction(call)
})
}
}
impl<
T: SigningTypes + SendTransactionTypes<LocalCall>,
C: AppCrypto<T::Public, T::Signature>,
LocalCall,
> SendUnsignedTransaction<T, LocalCall> for Signer<T, C, ForAll> {
type Result = Vec<(Account<T>, Result<(), ()>)>;
fn send_unsigned_transaction<TPayload, F>(
&self,
f: F,
f2: impl Fn(TPayload, T::Signature) -> LocalCall,
) -> Self::Result
where
F: Fn(&Account<T>) -> TPayload,
TPayload: SignedPayload<T> {
self.for_all(|account| {
let payload = f(account);
let signature = payload.sign::<C>()?;
let call = f2(payload, signature);
self.submit_unsigned_transaction(call)
})
}
}
#[derive(RuntimeDebug, PartialEq)]
pub struct Account<T: SigningTypes> {
pub index: usize,
pub id: T::AccountId,
pub public: T::Public,
}
impl<T: SigningTypes> Account<T> {
pub fn new(index: usize, id: T::AccountId, public: T::Public) -> Self {
Self { index, id, public }
}
}
impl<T: SigningTypes> Clone for Account<T> where
T::AccountId: Clone,
T::Public: Clone,
{
fn clone(&self) -> Self {
Self {
index: self.index,
id: self.id.clone(),
public: self.public.clone(),
}
}
}
pub trait AppCrypto<Public, Signature> {
type RuntimeAppPublic: RuntimeAppPublic;
type GenericPublic:
From<Self::RuntimeAppPublic>
+ Into<Self::RuntimeAppPublic>
+ TryFrom<Public>
+ Into<Public>;
type GenericSignature:
From<<Self::RuntimeAppPublic as RuntimeAppPublic>::Signature>
+ Into<<Self::RuntimeAppPublic as RuntimeAppPublic>::Signature>
+ TryFrom<Signature>
+ Into<Signature>;
fn sign(payload: &[u8], public: Public) -> Option<Signature> {
let p: Self::GenericPublic = public.try_into().ok()?;
let x = Into::<Self::RuntimeAppPublic>::into(p);
x.sign(&payload)
.map(|x| {
let sig: Self::GenericSignature = x.into();
sig
})
.map(Into::into)
}
fn verify(payload: &[u8], public: Public, signature: Signature) -> bool {
let p: Self::GenericPublic = match public.try_into() {
Ok(a) => a,
_ => return false
};
let x = Into::<Self::RuntimeAppPublic>::into(p);
let signature: Self::GenericSignature = match signature.try_into() {
Ok(a) => a,
_ => return false
};
let signature = Into::<<
Self::RuntimeAppPublic as RuntimeAppPublic
>::Signature>::into(signature);
x.verify(&payload, &signature)
}
}
pub trait SigningTypes: crate::Trait {
type Public: Clone
+ PartialEq
+ IdentifyAccount<AccountId = Self::AccountId>
+ core::fmt::Debug
+ codec::Codec
+ Ord;
type Signature: Clone
+ PartialEq
+ core::fmt::Debug
+ codec::Codec;
}
pub trait SendTransactionTypes<LocalCall> {
type Extrinsic: ExtrinsicT<Call=Self::OverarchingCall> + codec::Encode;
type OverarchingCall: From<LocalCall>;
}
pub trait CreateSignedTransaction<LocalCall>: SendTransactionTypes<LocalCall> + SigningTypes {
fn create_transaction<C: AppCrypto<Self::Public, Self::Signature>>(
call: Self::OverarchingCall,
public: Self::Public,
account: Self::AccountId,
nonce: Self::Index,
) -> Option<(Self::OverarchingCall, <Self::Extrinsic as ExtrinsicT>::SignaturePayload)>;
}
pub trait SignMessage<T: SigningTypes> {
type SignatureData;
fn sign_message(&self, message: &[u8]) -> Self::SignatureData;
fn sign<TPayload, F>(&self, f: F) -> Self::SignatureData where
F: Fn(&Account<T>) -> TPayload,
TPayload: SignedPayload<T>,
;
}
pub trait SendSignedTransaction<
T: SigningTypes + CreateSignedTransaction<LocalCall>,
C: AppCrypto<T::Public, T::Signature>,
LocalCall
> {
type Result;
fn send_signed_transaction(
&self,
f: impl Fn(&Account<T>) -> LocalCall,
) -> Self::Result;
fn send_single_signed_transaction(
&self,
account: &Account<T>,
call: LocalCall,
) -> Option<Result<(), ()>> {
let mut account_data = crate::Account::<T>::get(&account.id);
debug::native::debug!(
target: "offchain",
"Creating signed transaction from account: {:?} (nonce: {:?})",
account.id,
account_data.nonce,
);
let (call, signature) = T::create_transaction::<C>(
call.into(),
account.public.clone(),
account.id.clone(),
account_data.nonce
)?;
let res = SubmitTransaction::<T, LocalCall>
::submit_transaction(call, Some(signature));
if res.is_ok() {
account_data.nonce += One::one();
crate::Account::<T>::insert(&account.id, account_data);
}
Some(res)
}
}
pub trait SendUnsignedTransaction<
T: SigningTypes + SendTransactionTypes<LocalCall>,
LocalCall,
> {
type Result;
fn send_unsigned_transaction<TPayload, F>(
&self,
f: F,
f2: impl Fn(TPayload, T::Signature) -> LocalCall,
) -> Self::Result
where
F: Fn(&Account<T>) -> TPayload,
TPayload: SignedPayload<T>;
fn submit_unsigned_transaction(
&self,
call: LocalCall
) -> Option<Result<(), ()>> {
Some(SubmitTransaction::<T, LocalCall>
::submit_unsigned_transaction(call.into()))
}
}
pub trait SignedPayload<T: SigningTypes>: Encode {
fn public(&self) -> T::Public;
fn sign<C: AppCrypto<T::Public, T::Signature>>(&self) -> Option<T::Signature> {
self.using_encoded(|payload| C::sign(payload, self.public()))
}
fn verify<C: AppCrypto<T::Public, T::Signature>>(&self, signature: T::Signature) -> bool {
self.using_encoded(|payload| C::verify(payload, self.public(), signature))
}
}
#[cfg(test)]
mod tests {
use super::*;
use codec::Decode;
use crate::mock::{Test as TestRuntime, Call};
use sp_core::offchain::{testing, TransactionPoolExt};
use sp_runtime::testing::{UintAuthorityId, TestSignature, TestXt};
impl SigningTypes for TestRuntime {
type Public = UintAuthorityId;
type Signature = TestSignature;
}
type Extrinsic = TestXt<Call, ()>;
impl SendTransactionTypes<Call> for TestRuntime {
type Extrinsic = Extrinsic;
type OverarchingCall = Call;
}
#[derive(codec::Encode, codec::Decode)]
struct SimplePayload {
pub public: UintAuthorityId,
pub data: Vec<u8>,
}
impl SignedPayload<TestRuntime> for SimplePayload {
fn public(&self) -> UintAuthorityId {
self.public.clone()
}
}
struct DummyAppCrypto;
impl AppCrypto<UintAuthorityId, TestSignature> for DummyAppCrypto {
type RuntimeAppPublic = UintAuthorityId;
type GenericPublic = UintAuthorityId;
type GenericSignature = TestSignature;
}
fn assert_account(
next: Option<(Account<TestRuntime>, Result<(), ()>)>,
index: usize,
id: u64,
) {
assert_eq!(next, Some((Account {
index,
id,
public: id.into(),
}, Ok(()))));
}
#[test]
fn should_send_unsigned_with_signed_payload_with_all_accounts() {
let (pool, pool_state) = testing::TestTransactionPoolExt::new();
let mut t = sp_io::TestExternalities::default();
t.register_extension(TransactionPoolExt::new(pool));
UintAuthorityId::set_all_keys(vec![0xf0, 0xf1, 0xf2]);
t.execute_with(|| {
let result = Signer::<TestRuntime, DummyAppCrypto>
::all_accounts()
.send_unsigned_transaction(
|account| SimplePayload {
data: vec![1, 2, 3],
public: account.public.clone()
},
|_payload, _signature| {
Call
}
);
let mut res = result.into_iter();
assert_account(res.next(), 0, 0xf0);
assert_account(res.next(), 1, 0xf1);
assert_account(res.next(), 2, 0xf2);
assert_eq!(res.next(), None);
let tx1 = pool_state.write().transactions.pop().unwrap();
let _tx2 = pool_state.write().transactions.pop().unwrap();
let _tx3 = pool_state.write().transactions.pop().unwrap();
assert!(pool_state.read().transactions.is_empty());
let tx1 = Extrinsic::decode(&mut &*tx1).unwrap();
assert_eq!(tx1.signature, None);
});
}
#[test]
fn should_send_unsigned_with_signed_payload_with_any_account() {
let (pool, pool_state) = testing::TestTransactionPoolExt::new();
let mut t = sp_io::TestExternalities::default();
t.register_extension(TransactionPoolExt::new(pool));
UintAuthorityId::set_all_keys(vec![0xf0, 0xf1, 0xf2]);
t.execute_with(|| {
let result = Signer::<TestRuntime, DummyAppCrypto>
::any_account()
.send_unsigned_transaction(
|account| SimplePayload {
data: vec![1, 2, 3],
public: account.public.clone()
},
|_payload, _signature| {
Call
}
);
let mut res = result.into_iter();
assert_account(res.next(), 0, 0xf0);
assert_eq!(res.next(), None);
let tx1 = pool_state.write().transactions.pop().unwrap();
assert!(pool_state.read().transactions.is_empty());
let tx1 = Extrinsic::decode(&mut &*tx1).unwrap();
assert_eq!(tx1.signature, None);
});
}
#[test]
fn should_send_unsigned_with_signed_payload_with_all_account_and_filter() {
let (pool, pool_state) = testing::TestTransactionPoolExt::new();
let mut t = sp_io::TestExternalities::default();
t.register_extension(TransactionPoolExt::new(pool));
UintAuthorityId::set_all_keys(vec![0xf0, 0xf1, 0xf2]);
t.execute_with(|| {
let result = Signer::<TestRuntime, DummyAppCrypto>
::all_accounts()
.with_filter(vec![0xf2.into(), 0xf1.into()])
.send_unsigned_transaction(
|account| SimplePayload {
data: vec![1, 2, 3],
public: account.public.clone()
},
|_payload, _signature| {
Call
}
);
let mut res = result.into_iter();
assert_account(res.next(), 0, 0xf2);
assert_account(res.next(), 1, 0xf1);
assert_eq!(res.next(), None);
let tx1 = pool_state.write().transactions.pop().unwrap();
let _tx2 = pool_state.write().transactions.pop().unwrap();
assert!(pool_state.read().transactions.is_empty());
let tx1 = Extrinsic::decode(&mut &*tx1).unwrap();
assert_eq!(tx1.signature, None);
});
}
#[test]
fn should_send_unsigned_with_signed_payload_with_any_account_and_filter() {
let (pool, pool_state) = testing::TestTransactionPoolExt::new();
let mut t = sp_io::TestExternalities::default();
t.register_extension(TransactionPoolExt::new(pool));
UintAuthorityId::set_all_keys(vec![0xf0, 0xf1, 0xf2]);
t.execute_with(|| {
let result = Signer::<TestRuntime, DummyAppCrypto>
::any_account()
.with_filter(vec![0xf2.into(), 0xf1.into()])
.send_unsigned_transaction(
|account| SimplePayload {
data: vec![1, 2, 3],
public: account.public.clone()
},
|_payload, _signature| {
Call
}
);
let mut res = result.into_iter();
assert_account(res.next(), 0, 0xf2);
assert_eq!(res.next(), None);
let tx1 = pool_state.write().transactions.pop().unwrap();
assert!(pool_state.read().transactions.is_empty());
let tx1 = Extrinsic::decode(&mut &*tx1).unwrap();
assert_eq!(tx1.signature, None);
});
}
}